/*
 * Copyright 2003,2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.jakubiak.jpa.translator;

import eu.jakubiak.jpa.translator.annotations.Translatable;
import eu.jakubiak.jpa.translator.annotations.Translate;
import eu.jakubiak.jpa.translator.annotations.TranslatorFallowMany;
import eu.jakubiak.jpa.translator.annotations.TranslatorFallowOne;
import eu.jakubiak.jpa.translator.discriminator.Discriminator;
import eu.jakubiak.jpa.translator.provider.Provider;
import eu.jakubiak.jpa.translator.proxy.collections.TranslatorProxyCollection;
import eu.jakubiak.jpa.translator.proxy.collections.TranslatorProxyList;
import eu.jakubiak.jpa.translator.proxy.collections.TranslatorProxyMap;
import eu.jakubiak.jpa.translator.proxy.collections.TranslatorProxySet;
import eu.jakubiak.jpa.translator.serializer.Serializer;
import eu.jakubiak.utils.javabean.BeanProperty;
import eu.jakubiak.utils.javabean.BeanReflector;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * Translator class
 * 
 * 
 * @author jakubiak_a
 */
public class Translator {
    /**
     * Current locale for tranlated objects
     */
    private Locale locale;
    /**
     * Provider instance 
     * this override @Translate(provider)
     */
    private Provider provider;
    /**
     * Discriminator instance
     * this override @Translate(discriminator)
     */
    private Discriminator discriminator;
    /**
     * Serializer instance
     * this override @Translate(serializer)
     */
    private Serializer serializer;

    
    
    /**
     * Mark allready translated object
     * This interface is used to create proxy class
     */
    public static interface AlreadyTranslated {

        /**
         * get delegate for proxy object
         * this method shoud not be called directly
         * @param dummy unused parameter which helps method inteceptor to identify this method
         * @return
         */
        Object getDelegate(AlreadyTranslated dummy);

        /**
         * get translator for proxy object
         * this method shoud not be called directly
         * @param dummy unused parameter which helps method inteceptor to identify this method
         * @return
         */
        Translator getTranslator(AlreadyTranslated dummy);
    }

    
    private class TranslatorProxyMethodInterceptor implements MethodInterceptor {
        
        private Translator translator;
        private Object delegate;
        private BeanReflector reflector;

        public TranslatorProxyMethodInterceptor(Object delegate, Translator translator) {
            this.delegate = delegate;
            this.reflector = new BeanReflector(delegate);
            this.translator = translator;
        }

        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

            if (Translator.AlreadyTranslated.class.getMethod("getDelegate", Translator.AlreadyTranslated.class).equals(method)) {
                return this.delegate;
            }
            if (Translator.AlreadyTranslated.class.getMethod("getTranslator", Translator.AlreadyTranslated.class).equals(method)) {
                return this.translator;
            }

            if (method.getName().startsWith("set")) {
                if (reflector.containsMethod(method.getName())) {
                    BeanProperty property = reflector.getProperty(BeanReflector.lowerCaseFirst(method.getName().substring(3)));
                    if (property.isAnnotationPresent(Translate.class)) {
                        return set(property, args[0]);
                    }
                }
            }
            if (method.getName().startsWith("get")) {
                if (reflector.containsMethod(method.getName())) {
                    BeanProperty property = reflector.getProperty(BeanReflector.lowerCaseFirst(method.getName().substring(3)));
                    if (property.isAnnotationPresent(Translate.class)) {
                        return get(property);
                    }
                    if (property.isAnnotationPresent(TranslatorFallowOne.class)) {
                        TranslatorFallowOne tfo = property.getAnnotation(TranslatorFallowOne.class);
                        Class targetClass = tfo.targetClass();
                        if(void.class.equals(targetClass)) {
                            targetClass = property.getType();
                        }
                        return translator.translate(targetClass,get(property));
                    }
                    if (property.isAnnotationPresent(TranslatorFallowMany.class)) {
                        if (!property.getType().isInterface()) {
                            throw new RuntimeException("TranslatorFallowMany require interface: " + property.getType());
                        } else {
                            Object returnValue = get(property);
                            TranslatorFallowMany tfm = property.getAnnotation(TranslatorFallowMany.class);
                            Class targetClass = tfm.targetClass();
                            if(void.class.equals(targetClass)) {
                                Type type = property.getParameterizedType().getActualTypeArguments()[0];
                                if(type==null) {
                                    throw new RuntimeException("Cannot read a collection type");
                                }
                                targetClass = (Class)type;
                            }
                            if (returnValue != null) {
                                if (returnValue instanceof Map) {
                                    return new TranslatorProxyMap(targetClass, translator, (Map) returnValue);
                                } else if (returnValue instanceof Set) {
                                    return new TranslatorProxySet(targetClass, translator, (Set) returnValue);
                                } else if (returnValue instanceof List) {
                                    return new TranslatorProxyList(targetClass, translator, (List) returnValue);
                                } else if (returnValue instanceof Collection) {
                                    return new TranslatorProxyCollection(targetClass, translator, (Collection) returnValue);
                                } else {
                                    throw new RuntimeException("Unsupported collection type: " + returnValue.getClass().getName());
                                }
                            }
                        }
                    }
                }
            }
            if (method.getName().startsWith("is")) {
                if (reflector.containsMethod(method.getName())) {
                    BeanProperty property = reflector.getProperty(BeanReflector.lowerCaseFirst(method.getName().substring(2)));
                    if (property.isAnnotationPresent(Translate.class)) {
                        return get(property);
                    }
                }
            }
            return method.invoke(delegate, args);
        }

        private Object get(BeanProperty property) throws Throwable {
            return translator.read(property, delegate);
        }

        private Object set(BeanProperty property, Object value) throws Throwable {
            translator.store(property, delegate, value);
            return null;
        }
    }
    
    
    
    

    /**
     * Create new translator
     * @param locale
     */
    public Translator(Locale locale) {
        this.locale = locale;
        if (this.locale == null) {
            throw new RuntimeException("Error while initializing translator");
        }
    }


    /**
     * Get current locale
     * @return current locale used in translator
     */
    public Locale getLocale() {
        return locale;
    }

    /**
     * Create translated proxy object
     * 
     * @param object for translation
     * @return proxy object 
     */
    public <T> T translate(Class<T> clazz, T object) {
        if (object == null) {
            // I don't know how to read null
            return null;
        }

        if (isTranslated(object)) {
            // I want to translate proxy only for objects which wasn't allready translated
            // this object was allready translated
            return object;
        }

        if (!clazz.isAnnotationPresent(Translatable.class)) {
            // This object is not translatable
            return object;
        }


        MethodInterceptor methodInterceptor = new TranslatorProxyMethodInterceptor(object, this);
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setInterfaces(new Class[]{AlreadyTranslated.class});
        enhancer.setCallback(methodInterceptor);
        return (T)enhancer.create();
    }

    /**
     * Check, if an object is translated
     * @param object to test
     * @return true if object is translated
     */
    public static boolean isTranslated(Object object) {
        return object instanceof AlreadyTranslated;
    }

    /**
     * Get delegate object for translated proxy object
     * @param translatedObject
     * @return
     */
    public static Object getDelegate(Object translatedObject) {
        AlreadyTranslated proxy = (AlreadyTranslated) translatedObject;
        return proxy.getDelegate(proxy);
    }
    
    /**
     * Get translator used to translate object
     * @param translatedObject
     * @return
     */
    public static Translator getTranslator(Object translatedObject) {
        AlreadyTranslated proxy = (AlreadyTranslated) translatedObject;
        return proxy.getTranslator(proxy);
    }
    
    

    public Discriminator getDiscriminator() {
        return discriminator;
    }

    public void setDiscriminator(Discriminator discriminator) {
        this.discriminator = discriminator;
    }

    public Provider getProvider() {
        return provider;
    }

    public void setProvider(Provider provider) {
        this.provider = provider;
    }

    public Serializer getSerializer() {
        return serializer;
    }

    public void setSerializer(Serializer serializer) {
        this.serializer = serializer;
    }

    
    
    
    /**
     * Read translation from provider
     * @param property
     * @param object
     * @return
     * @throws java.lang.IllegalAccessException
     * @throws java.lang.IllegalArgumentException
     * @throws java.lang.reflect.InvocationTargetException
     * @throws java.lang.InstantiationException
     */
    private Object read(BeanProperty property, Object object)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {

        if (!property.isAnnotationPresent(Translate.class)) {
            return property.get(object);
        }

        Translate translatable = (Translate) property.getAnnotation(Translate.class);


        TranslationKey key = createKey(property, object);

        String translatedText = getProviderFor(translatable).get(key);

        Object returnValue = getSerializerFor(translatable).deserialize(property.getType(), translatedText);
        if (returnValue != null) {
            return returnValue;
        }

        return property.get(object);
    }

    /**
     * Persist translation in provider
     * @param property
     * @param object
     * @param translatedValue
     * @throws java.lang.IllegalAccessException
     * @throws java.lang.IllegalArgumentException
     * @throws java.lang.reflect.InvocationTargetException
     * @throws java.lang.InstantiationException
     */
    private void store(BeanProperty property, Object object, Object translatedValue)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {

        if (!property.isAnnotationPresent(Translate.class)) {
            property.set(object, translatedValue);
            return;
        }

        Translate translatable = (Translate) property.getAnnotation(Translate.class);
        String translatedString = getSerializerFor(translatable).serialize(translatedValue);

        TranslationKey key = createKey(property, object);
        getProviderFor(translatable).set(key, translatedString);
    }

    /**
     * Get provider 
     * XXX kod tej metody powinień być przeniesiony do osobnej klasy
     * @param translate
     * @return
     * @throws java.lang.InstantiationException
     * @throws java.lang.IllegalAccessException
     */
    private Provider getProviderFor(Translate translate) throws InstantiationException, IllegalAccessException {
        if (getProvider() != null) {
            return getProvider();
        }
        if(!void.class.equals(translate.provider())) {
            return translate.provider().newInstance();    
        }
        return TranslatorProperties.getProviderClass().newInstance();
    }

    /**
     * Get serializer
     * XXX kod tej metody powinień być przeniesiony do osobnej klasy
     * @param translate
     * @return
     * @throws java.lang.InstantiationException
     * @throws java.lang.IllegalAccessException
     */
    private Serializer getSerializerFor(Translate translate) throws InstantiationException, IllegalAccessException {
        if (getSerializer() != null) {
            return getSerializer();
        }
        if(!void.class.equals(translate.serializer())) {
            return translate.serializer().newInstance();    
        }
        return TranslatorProperties.getSerializerClass().newInstance();
    }

    /**
     * Get discriminator
     * XXX kod tej metody powinień być przeniesiony do osobnej klasy
     * @param translate
     * @return
     * @throws java.lang.InstantiationException
     * @throws java.lang.IllegalAccessException
     */
    private Discriminator getDiscriminatorFor(Translate translate) throws InstantiationException, IllegalAccessException {
        if (getDiscriminator() != null) {
            return getDiscriminator();
        }
        if(!void.class.equals(translate.discriminator())) {
            return translate.discriminator().newInstance();    
        }
        return TranslatorProperties.getDiscriminatorClass().newInstance();
    }
    
    
    /**
     * Creating key for find it in translation table
     * @param property
     * @param object
     * @return
     * @throws java.lang.InstantiationException
     * @throws java.lang.IllegalAccessException
     * @throws java.lang.IllegalArgumentException
     * @throws java.lang.reflect.InvocationTargetException
     */
    private TranslationKey createKey(BeanProperty property, Object object) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Translate translatable = (Translate) property.getAnnotation(Translate.class);
        return new TranslationKey(property, object, getLocale(), getSerializerFor(translatable), getDiscriminatorFor(translatable));
    }

    
    
    @Override
    public boolean equals(Object obj) {
        return this == obj;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        return hash;
    }
    
}
